Assignment 3 Write-up
Name: Ge Jin
Student ID: 3037754475
Link: https://cal-cs184-student.github.io/hw-webpages-sp24-maxwelljin/hw3/index.html
Overview
In this project, we are rendering a realistic scenario, which comprises five different parts: ray generation and scene intersection, bounding volume hierarchy, direct illumination and global illumination, and adaptive sampling. In ray generation, we randomly cast rays from the camera, and we need to determine if these rays intersect with any objects, which requires developing efficient intersection algorithms for quick verification. Regarding the bounding volume hierarchy, it acts as an acceleration structure to speed up the process, as iterating through all possible object intersections would be impractical. For direct and global illumination, we employ the rendering equation to calculate the incoming light and use the BRDF (Bidirectional Reflectance Distribution Function) to determine how much light is reflected back to the camera. Finally, given the complexity of the lighting setup, we focus on sampling the parts with the most variance and strive to make the image rendering converge faster.
Part 1
In the ray generation algorithm, we start by casting a ray from the camera's position through each pixel on the image plane. This process involves determining the direction of each ray so that it passes through the corresponding pixel (the specific position within the pixel is determined through sampling) and extends into the scene. The direction is usually calculated based on the camera's orientation, field of view, and the position of the pixel within the image plane. This method ensures that the ray accurately represents the path that light would take to reach that specific point on the camera's sensor.
To test the triangle intersection, we first calculate the surface on which the triangle resides. Then, we calculate the time at which the ray intersects with the plane. We use this time to determine the specific position where the ray intersects with the plane. Next, we need to verify if the point is within the triangle. To do this, we use the barycentric coordinate system, which helps determine whether the intersection point of the ray with the triangle's plane lies within the triangle's boundaries. The barycentric coordinates, alpha, beta, and gamma, represent the weights or influences of the triangle's vertices on the specific point within the triangle. If alpha, beta, and gamma are all between 0 and 1, it means the ray intersects within the triangle.
(b) Images with normal shading for small dae files:


Part 2
(a)
The BVH construction algorithm I use aims to split the volume to minimize the expected search time until it contains the maximum number of elements. The primary strategy involves using the mean value of the geometry bounds as the threshold for splitting. To determine the axis along which to split, I calculate the variance for each axis (X, Y, and Z) and select the one with the largest variance for splitting. This method aims to balance the number of primitives in each subtree, leading to more efficient traversal during rendering.
(b)

(c)
In practice, moderately complex geometry benefits significantly from BVH acceleration. For instance, on my M2 computer, rendering a sphere with BVH acceleration takes only 0.54 seconds, whereas without BVH, it requires around 55.8 seconds. The efficiency of BVH stems from its logarithmic time complexity (log(N)) for each light ray, which substantially reduces the number of intersection tests needed during ray tracing, especially in scenes with a large number of primitives.
Part 3
(a) In the direct lighting function, we implement the BRDF function for diffuse materials, which reflects light equally in all directions. The process is straightforward: we use a sampler to cast rays in various directions to measure the amount of light reflected at the sample point. We use the BRDF to represent the relationship between incoming and outgoing light, based on the material's properties. Use the BRDF as a part of the rendering equation allows us to compute the direct lighting.
To estimate direct lighting, we developed two methods: hemisphere sampling and direct lighting importance. In estimating the direct light from the hemisphere, we aim to account for all received lights. To do this, we sample n different lights and randomly cast rays to the hemisphere at the intersection point. To avoid self-intersection, we set the sample ray's minimum time as EPS_F. After calling bvh->intersect with the sample ray, we obtain the emission from the new sample intersection point where our cast sample ray intersects. Then, we add the output light with the light intensity of the sample ray, multiply by the BSDF at the intersection point, and divide by the PDF. Since the sampling is on a uniform sphere, it's just 1 over 2π (the surface area of a unit sphere is 4π).
To implement importance lighting, we cast light only to the light source, whether it's a point light source or an area light source. For a point light source, we only sample once. For an area light source, we use the sampleL function to get the sample ray direction, its distance to light, and the PDF for this sample. If the sample ray does not intersect with anything, it means it will directly hit the light source, and we simply add its light intensity. Otherwise, if it hits something, we sample from the emission of the intersection point. We use a similar equation as in hemisphere sampling to accumulate the output light.
(b)

Above: Uniform Hemisphere sampling

Above: Importance sampling
(c)

Images: 1, 4, 16, 64 lights (the top is 1 light)
Generally, casting more light leads to better results.
(d) Comparing uniform hemisphere sampling and importance lighting sampling reveals that the latter yields significantly better results. Importance lighting sampling casts light primarily in the direction where light is heading, which enhances our ability to track point light sources. Uniform hemisphere sampling, on the other hand, is prone to noise because it casts rays into areas without light, creating noise that affects image quality.
Part 4
(a) In the indirect lighting function, I use the rendering equation to simulate higher-order lighting. The rendering equation shows that the output light at a point p in direction w0 is the sum of the emission from p in w0 and the reflected light.
Specifically, I've modified the at_least_one_bounce_radiance function. This function takes a ray and an intersection point as inputs. We first estimate the direct lighting at this intersection point through the importance lighting function we just implemented. Then, to calculate the outgoing light due to reflection, we need to sample the reflected light using Monte Carlo integration and multiply it by the BRDF. To sample the reflected light, we cast a ray in a chosen direction, and if it intersects with an object, we recursively call at_least_one_bounce_radiance for the new sample ray, multiplying by the BSDF value and the cosine of the angle between the normal and the direction of the reflected light, and dividing by the PDF to account for importance sampling.
To avoid infinite recursion, we terminate the recursive call when we reach the maximum depth. However, this can introduce bias into our sampling process. Therefore, we use the Russian Roulette rendering approach, where we randomly terminate the recursive process to balance between efficiency and accuracy.
(b) Sample rendering:
(c)

Only Direct
Only Indirect
(d)

Images: From max ray depth 0-5 (level 0 is on the top)
In the 2nd bounce of light, I observed the light reflecting from the ground onto the bunny, which resulted in the bottom part of the bunny appearing illuminated and significantly brighter than in the original images. This illumination enhancement is particularly notable, as it indicates the indirect lighting effect. Additionally, there is a distinct light presence on the top wall, which suggests that the light is not only reflecting from the ground but also bouncing off the other three walls, creating a more complex and realistic light within the scene.
In the 3rd bounce of light, the scene becomes much darker Since there’s substantial reduction in direct light reaching the bunny, the bunny appears almost entirely black, with only faint illumination due to light diffusion. The light intensity during this bounce is considerably smaller compared to the 2nd bounce, indicating a rapid decrease in light energy as it continues to bounce within the environment. Most of the residual light is scattered onto the walls, further diminishing the direct illumination on the bunny and contributing to the overall dimming of the scene.
(e)

Images: From max_ray_depth 0-5 (level 0 is on the top)
As we increase the max_ray_depth, the lighting in the shadows becomes significantly more realistic, and the brightness of the image more closely resembles real-world conditions. Additionally, increasing from one bounce to two bounces successfully renders the bottom lighting of the bunny.
(f)

Images: From max_ray_depth 0-100 (level 0 is on the top) with Russian Roulette
(g)

Images: Sample rate from 1-1024 (1 is on the top)
When we increase the sample-per-pixel rate, it can significantly reduce the noise in the image. This improvement occurs because if we cast a light from the camera angle and that light does not hit any light source (even though we use importance sampling to mitigate this issue), it will create black dots in the image. By sampling more per pixel, we can achieve higher quality and more realistic images.
Part 5
(a)
In adaptive sampling, our goal is to sample more in areas where noise is more frequent, or in other words, where the lighting conditions are more complex. The algorithm is straightforward: we trace the samples we've already collected and calculate their variance and mean. We define the variable I as 1.96 * sigma / sqrt(mean) and compare it to our maxTolerance times the average values.
In the implementation, we sample the pixels in batches. This means we calculate the initial mean and variance in the first batch and check if the variance converges to a point where the output is stable. If it's not stable, we continue sampling until we exhaust all available maximum samples. By utilizing this approach, we can set the sample number to a relatively large number. For example, in rendering images for this section, we use 2048 samples instead of 1024 because, in most parts, it won't really utilize all available samples, allowing the algorithm to focus only on the more important areas.
(b)

In the images, we can see the algorithm heavily samples in areas like the bottom of the bunny and its shadows. Since the 3D shape of the bunny is irregular and complex, the light may reflect multiple times, requiring more samples to converge. Similarly, in scenarios with the bunny and sphere, we observe heavy sampling in the shadows and at the base on the top, indicating these are areas of complex lighting that benefit from additional sampling.